using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Collections;

namespace WMVConvert.Metadata
{
	public class WMAttribute
	{
		private Editor editor = null;

		/// <summary>The number of the stream from which to get metadata.</summary>
		internal const ushort DEFAULT_STREAM_NUMBER = 0;
		public const ushort ALL_STREAMS = 0xFFFF;
		public const ushort AUDIO_STREAM = 0x0001;
		public const ushort VIDEO_STREAM = 0x0002;

		private string name;
		private ItemType type;
		private Type subType;
		private ushort streamNumber;
		private ushort langIndex;

		internal WMAttribute(Editor editor,string name, ItemType type, ushort streamNumber, Type subType)
		{
			Init(editor, name, type, streamNumber, subType);
		}

		private void Init(Editor editor, string name, ItemType type, ushort streamNumber, Type subType)
		{
			this.editor = editor;
			this.name = name;
			this.type = type;
			this.subType = subType;
			this.streamNumber = streamNumber;
			this.langIndex = 0;
		}

        public IWMBinary CreateBinaryFromString(string stringValue)
		{
			if (subType != null)
			{
                return Activator.CreateInstance(subType, new object[] { stringValue }) as IWMBinary;
			}
			return null;
		}

		public string Name
		{
			get { return name; }
		}

		public ItemType Type
		{
			get { return type; }
		}

		public Type SubType
		{
			get { return subType; }
		}

		public ushort StreamNumber
		{
			get { return streamNumber; }
		}

		public ushort LangIndex
		{
			get { return langIndex; }
			set { langIndex = value; }
		}

		private IEnumerable GetMultipleValues()
		{
			ushort[] indices = this.Indices;
			for (int i = 0; i < indices.Length; i++)
			{
				yield return GetValue(indices[i]);
			}
			yield break;
		}

		private object value;

		public object Value
		{
			get
			{
				if (value == null)
				{
					ushort[] indices = this.Indices;
					if (indices.Length == 1)
					{
						value = GetValue(indices[0]);
					}
					else if (indices.Length > 0)
					{
						value = GetMultipleValues();
					}
					else
					{
						value = null;
					}
				}
				if (value == null)
				{
					return this.EmptyValue;
				}
				return value;
			}
			set
			{
				if (value == null)
				{
					throw new ArgumentNullException("value");
				}
				ushort[] indices = this.Indices;
				IEnumerable enumValue = value as IEnumerable;
				if (enumValue != null && value.GetType()!=typeof(string))
				{
					ushort i = 0;
					foreach(object v in enumValue)
					{
						if (i < indices.Length)
						{
							ModifyValue(indices[i++], v);
						}
						else
						{
							AddValue(v);
						}
					}
					for (; i < indices.Length; i++)
					{
						DeleteValue(i);
					}
				}
				else if ( indices.Length > 0 )
				{
					ModifyValue(indices[0], value);
				}
				else
				{
					AddValue(value);
				}
			}
		}

		private object EmptyValue
		{
			get
			{
				switch (this.type)
				{
					case ItemType.BOOL: return false;
					case ItemType.DWORD: return 0;
					case ItemType.GUID:
						{
							if (this.subType != null)
							{
								return Activator.CreateInstance(this.subType);
							}
							return Guid.Empty;
						}
					case ItemType.QWORD: return 0L;
					case ItemType.STRING: return string.Empty;
					case ItemType.WORD: return 0;
					case ItemType.BINARY:
						{
							if (this.subType != null)
							{
								return Activator.CreateInstance(this.subType);
							}
							return null;
						}
				}
				return null;
			}
		}

		internal ushort[] Indices
		{
			get
			{
				ushort langIndex = 0;
				ushort count = 0;
				this.editor.HeaderInfo.GetAttributeIndices(this.StreamNumber, this.Name, ref langIndex, null, ref count);
				if (count > 0)
				{
					ushort[] indices = new ushort[count];
					this.editor.HeaderInfo.GetAttributeIndices(this.StreamNumber, this.Name, ref langIndex, indices, ref count);
					return indices;
				}
				return new ushort[]{};
			}
		}

		public void DeleteValue()
		{
			ushort[] indices = this.Indices;
			foreach (ushort index in indices)
			{
				DeleteValue(index);
			}
		}

		private void DeleteValue(ushort index)
		{
			this.editor.Dirty = true;
			AsfError hr = this.editor.HeaderInfo.DeleteAttribute(this.StreamNumber, index);
			if (hr != AsfError.OK)
			{
				switch (hr)
				{
					case AsfError.NS_E_FILE_WRITE:
						break;
					default:
						Trace.TraceError("Unknown error 0x{1:x} deleting attribute {0}", this.Name, (int)hr);
						break;
				}
			}
		}

		private object GetValue(ushort index)
		{
			ushort langIndex = 0;
			ushort nameLength = (ushort)((this.Name.Length + 1) * 2);
			ItemType type;
			uint valueLength = 0;
			IntPtr value = IntPtr.Zero;
			editor.HeaderInfo.GetAttributeByIndexEx(
				this.StreamNumber, index,
				this.Name, ref nameLength,
				out type, out langIndex, value, ref valueLength);
			value = Marshal.AllocCoTaskMem((int)valueLength);
			try
			{
				editor.HeaderInfo.GetAttributeByIndexEx(
					this.StreamNumber, index,
					this.Name, ref nameLength,
					out type, out langIndex, value, ref valueLength);
				return this.ParseAttributeValue(value, valueLength);
			}
			finally
			{
				if (value != IntPtr.Zero)
				{
					Marshal.FreeCoTaskMem(value);
					value = IntPtr.Zero;
				}
			}
		}

		/// <summary>Gets or sets the value of the attribute.</summary>
		private ushort AddValue(object value)
		{
            IWMBinary binary = value as IWMBinary;
            if (binary == null)
            {
                binary = this.TranslateAttributeToUnmanaged(value);
            }

            if (binary == null)
            {
                throw new ArgumentOutOfRangeException("value", value, "Value cannot be converted to IWMBinary");
            }
			ushort index = 0;
			this.editor.Dirty = true;
			AsfError hr = this.editor.HeaderInfo.AddAttribute(this.StreamNumber, this.Name, out index, this.Type, this.LangIndex, binary.ValueData, (ushort)binary.ValueLength);
			return index;
		}

		/// <summary>Gets or sets the value of the attribute.</summary>
		private void ModifyValue(ushort index, object value)
		{
			IWMBinary binary = this.TranslateAttributeToUnmanaged(value);
			ushort langIndex = 0;
			this.editor.Dirty = true;
			this.editor.HeaderInfo.ModifyAttribute(this.StreamNumber, index, this.Type, langIndex, binary.ValueData, (ushort)binary.ValueLength);
		}

		public bool Exist
		{
			get
			{
				return this.Indices.Length > 0;
			}
		}

		/// <summary>Converts a value to the target type and gets its byte representation.</summary>
		/// <param name="item">The item whose value is to be translated.</param>
		/// <param name="valueData">The output byte array.</param>
		internal IWMBinary TranslateAttributeToUnmanaged(object value)
		{
			switch (this.type)
			{
				case ItemType.DWORD:
					if (value.GetType() == typeof(string))
					{
						value = int.Parse(value.ToString());
					}
					return new WMBinary<Int32>((Int32)value);

				case ItemType.WORD:
					if (value.GetType() == typeof(string))
					{
						value = short.Parse(value.ToString());
					}
					return new WMBinary<Int16>((Int16)value);

				case ItemType.QWORD:
					if (value.GetType() == typeof(string))
					{
						value = long.Parse(value.ToString());
					}
					return new WMBinary<Int64>((Int64)value);

				case ItemType.BOOL:
					if (value.GetType() == typeof(string))
					{
						value = bool.Parse(value.ToString());
					}
					return new WMBinary<Int16>((Int16)((bool)value ? 1 : 0));

				case ItemType.GUID:
					if (this.subType != null)
					{
						IWMBinary binary = (IWMBinary)Activator.CreateInstance(this.subType, value);
						return binary;
					}
					else if (value.GetType() == typeof(string))
					{
						value = new Guid(value.ToString());
					}
					return new WMBinary<Guid>((Guid)value);

				case ItemType.STRING:
					return new WMString(value.ToString());

				case ItemType.BINARY:
					if (value is IWMBinary)
					{
						return (IWMBinary)value;
					}
					else if (this.subType != null)
					{
						IWMBinary binary = (IWMBinary)Activator.CreateInstance(this.subType, value);
						return binary;
					}
					break;
			}
			throw new ArgumentOutOfRangeException("value");
		}

		internal void Reset()
		{
			this.value = null;
			editor = null;
		}

		/// <summary>Gets the value of the specified attribute.</summary>
		/// <param name="itemType">The type of the attribute.</param>
		/// <param name="valueData">The byte array to be parsed.</param>
		internal object ParseAttributeValue(IntPtr valueData,uint valueDataLength)
		{
			if (!Enum.IsDefined(typeof(ItemType), type))
				throw new ArgumentOutOfRangeException("itemType");
			if (valueData == null) throw new ArgumentNullException("valueData");

			// Convert the attribute value to a byte array based on the item type.
			switch (type)
			{
				case ItemType.STRING: return Marshal.PtrToStringUni(valueData);
				case ItemType.BOOL: return Marshal.ReadInt16(valueData);
				case ItemType.DWORD: return Marshal.ReadInt32(valueData);
				case ItemType.QWORD: return Marshal.ReadInt64(valueData);
				case ItemType.WORD: return Marshal.ReadInt16(valueData);
				case ItemType.GUID:
					{
						Guid guidValue = (Guid)Marshal.PtrToStructure(valueData, typeof(Guid));
						if (this.subType != null)
						{
							return Activator.CreateInstance(this.subType, guidValue);
						}
						else
						{
							return guidValue;
						}
					}
				case ItemType.BINARY:
					{
						if (this.subType == null)
						{
							Trace.TraceWarning("Attributte {0} not supported", this.Name);
							return null;
						}
						Type subType = this.subType;
						if (!typeof(IWMBinary).IsAssignableFrom(subType))
						{
							subType = typeof(WMBinary<>).MakeGenericType(subType);
						}
						return Activator.CreateInstance(subType, valueData, valueDataLength);
					}
				default: throw new ArgumentOutOfRangeException("itemType");
			}
		}
	}
}
